//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#include "private.h"
#include <buola/io/cfdpoller.h>
#include <buola/algorithm/comparison.h>
#include <buola/dbus/cproxy.h>
#include <buola/io.h>

namespace buola { namespace dbus {

//! \name DBus callbacks
//@{

static dbus_bool_t DBusAddWatchCB(DBusWatch *pWatch,void *pBus)
{
    CBus *lBus=(CBus*)pBus;

    if(!dbus_watch_get_enabled(pWatch)) return true;

    int lDFlags=dbus_watch_get_flags(pWatch);
    int lFD=dbus_watch_get_unix_fd(pWatch);

    io::ENotify lNotify=io::ENotify::NONE;

    if(lDFlags&DBUS_WATCH_READABLE)
        lNotify|=io::ENotify::READ;
    if(lDFlags&DBUS_WATCH_WRITABLE)
        lNotify|=io::ENotify::WRITE;

    io::get_main_fd_poller().AddFDListener(lBus,lFD,lNotify,pWatch,io::ENotifyOp::OR);
    return true;
}

static void DBusWatchToggledCB(DBusWatch*,void*)
{
}

static void DBusRemoveWatchCB(DBusWatch*,void*)
{
}

static dbus_bool_t DBusAddTimeoutCB(DBusTimeout*,void*)
{
    return true;
}

static void DBusTimeoutToggledCB(DBusTimeout*,void*)
{
}

static void DBusRemoveTimeoutCB(DBusTimeout*,void*)
{
}

static DBusHandlerResult DBusHandleMessageCB(DBusConnection*,
                            DBusMessage *pM,void *pConnection)
{
    bool lProcessed=false;
    CBus *lBus=(CBus*)pConnection;

    int lType=dbus_message_get_type(pM);

    if(lType==DBUS_MESSAGE_TYPE_METHOD_CALL)
    {
        CMessage *lM=new CMessage(nReceived,lBus,pM);
        lProcessed=lBus->ProcessMethodCall(lM);
        delete lM;
    }
    else if(lType==DBUS_MESSAGE_TYPE_METHOD_RETURN||lType==DBUS_MESSAGE_TYPE_ERROR)
    {
        int lSerial=dbus_message_get_reply_serial(pM);
        CMessage *lReply=lBus->GetQueuedReply(lSerial);
        if(lReply)
        {
            lProcessed=true;
            lReply->FillEmpty(pM);
//TODO:            if(lType==DBUS_MESSAGE_TYPE_METHOD_RETURN)
//                lReply->sReplyReceived();
        }
    }
    else if(lType==DBUS_MESSAGE_TYPE_SIGNAL)
    {
        CMessage *lM=new CMessage(nReceived,lBus,pM);
        lProcessed=lBus->ProcessSignal(lM);
        delete lM;
    }

    if(lProcessed)
        return DBUS_HANDLER_RESULT_HANDLED;
    else
        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

//@}

////////////////////////////////////////////////////////////////
///////////////////////////// CBus /////////////////////////////
////////////////////////////////////////////////////////////////

CBus::CBus(EBusType pType)
    :   mP(new SPrivate),
        mType(pType)
{
    Connect();
}

CBus::~CBus()
{
    if(mP->mConnection)
        dbus_connection_unref(mP->mConnection);
}

void CBus::Connect()
{
    DBusError lError;

    dbus_error_init(&lError);
    DBusBusType lType;
    if(mType==SESSION)
    {
        lType=DBUS_BUS_SESSION;
    }
    else
    {
        lType=DBUS_BUS_SYSTEM;
    }
    mP->mConnection=dbus_bus_get(lType,&lError);

    if(!mP->mConnection)
    {
        throw XInternal(I18N("Can't connect to dbus"));
    }

    dbus_connection_set_watch_functions(mP->mConnection,
        DBusAddWatchCB,DBusRemoveWatchCB,DBusWatchToggledCB,this,nullptr);
    dbus_connection_set_timeout_functions(mP->mConnection,
        DBusAddTimeoutCB,DBusRemoveTimeoutCB,DBusTimeoutToggledCB,this,nullptr);

    dbus_connection_add_filter(mP->mConnection,DBusHandleMessageCB,
                                    this,nullptr);

    get_main_event_loop().AddFunction(CEventLoop::PRIORITY_LOW,this);
}

void CBus::AddObject(CObjectBase *pObject)
{
    mObjects.push_back(pObject);
}

void CBus::RemoveObject(CObjectBase *pObject)
{
    mObjects.erase(std::find(mObjects.begin(),mObjects.end(),pObject));
}

void CBus::AddObjectTree(CObjectBase *pObjectTree)
{
    mObjectTrees.push_back(pObjectTree);
}

void CBus::RemoveObjectTree(CObjectBase *pObjectTree)
{
    mObjectTrees.erase(std::find(mObjectTrees.begin(),mObjectTrees.end(),pObjectTree));
}

std::string CBus::GetObjectMatchRule(const std::string &pPath)
{
    std::string lMatch("type='signal',path='");
    lMatch+=pPath;
    lMatch+="'";
    return lMatch;
}

std::string CBus::GetInterfaceMatchRule(const std::string &pPath,const std::string &pInterface)
{
    std::string lMatch("type='signal',path='");
    lMatch+=pPath;
    lMatch+="',interface='";
    lMatch+=pInterface;
    lMatch+="'";
    return lMatch;
}

std::string CBus::GetSignalMatchRule(const std::string &pPath,const std::string &pInterface,const std::string &pSignal)
{
    std::string lMatch("type='signal',path='");
    lMatch+=pPath;
    lMatch+="',interface='";
    lMatch+=pInterface;
    lMatch+="',member='";
    lMatch+=pSignal;
    lMatch+="'";
    return lMatch;
}

CBus::tSignal *CBus::RegisterObjectMatch(const std::string &pPath)
{
    auto lM=mObjectMatches.find(pPath);
    if(lM!=mObjectMatches.end()) return lM->second;

    std::string lRule=GetObjectMatchRule(pPath);

    dbus_bus_add_match(mP->mConnection,lRule.c_str(),nullptr);
    tSignal *lSignal=new tSignal;
    mObjectMatches[pPath]=lSignal;

    return lSignal;
}

CBus::tSignal *CBus::RegisterInterfaceMatch(const std::string &pPath,const std::string &pInterface)
{
    std::string lHash=pPath+pInterface;
    auto lM=mObjectMatches.find(lHash);
    if(lM!=mObjectMatches.end()) return lM->second;

    std::string lRule=GetInterfaceMatchRule(pPath,pInterface);

    dbus_bus_add_match(mP->mConnection,lRule.c_str(),nullptr);
    tSignal *lSignal=new tSignal;
    mObjectMatches[lHash]=lSignal;

    return lSignal;
}

bool CBus::RegisterService(const std::string &pService,ERegisterFlags pFlags)
{
    DBusError lError;
    dbus_error_init(&lError);

    int lFlags=0;
    if(pFlags&REGISTER_ALLOW_REPLACEMENT)
        lFlags|=DBUS_NAME_FLAG_ALLOW_REPLACEMENT;
    if(pFlags&REGISTER_REPLACE_EXISTING)
        lFlags|=DBUS_NAME_FLAG_REPLACE_EXISTING;

    int lReturn=dbus_bus_request_name(mP->mConnection,pService.c_str(),0,&lError);
    if(lReturn==DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER||
    lReturn==DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)
    {
        return true;
    }

    return false;
}

void CBus::OnFDListener(int pFD,io::ENotify pFlags,void *pWatch)
{
    int lDFlags=0;

    if(pFlags&io::ENotify::READ)
        lDFlags|=DBUS_WATCH_READABLE;
    if(pFlags&io::ENotify::WRITE)
        lDFlags|=DBUS_WATCH_WRITABLE;

    dbus_watch_handle((DBusWatch*)pWatch,lDFlags);
    msg_debug<25>() << "Data available in " << pFD << " flags " << (int)pFlags <<
            " " << pWatch << "\n";
}

bool CBus::OnLoopFunction()
{
    while(dbus_connection_dispatch(mP->mConnection)==
                            DBUS_DISPATCH_DATA_REMAINS) {}
    return true;
}

CObjectBase *CBus::GetTarget(CMessage *pM)
{
    std::string lPath(pM->GetPath());

    for(int i=0;i<mObjects.size();i++)
    {
        if(mObjects[i]->DBusGetPath()==lPath)
        {
            return mObjects[i];
        }
    }

    for(int i=0;i<mObjectTrees.size();i++)
    {
        if(lPath==mObjectTrees[i]->DBusGetPath()||starts_with(lPath,mObjectTrees[i]->DBusGetPath()+'/'))
        {
            return mObjectTrees[i];
        }
    }

    return nullptr;
}

bool CBus::ProcessMethodCall(CMessage *pM)
{
    CObjectBase *lTarget=GetTarget(pM);

    if(!lTarget) return false;

    try
    {
        if(!lTarget->DBusCall(pM))
        {
            PMessage lError=pM->BuildError(DBUS_ERROR_UNKNOWN_METHOD,
                        "method "+pM->GetMember()+" doesn't exist for object at "+pM->GetPath());
            lError->Send();
        }
    }
    catch(XWrongParams &pE)
    {
        PMessage lError=pM->BuildError(DBUS_ERROR_INVALID_ARGS,pE.what());
        lError->Send();
    }
    catch(std::exception &pE)
    {
        PMessage lError=pM->BuildError(DBUS_ERROR_FAILED,pE.what());
        lError->Send();
    }

    return true;
}

bool CBus::ProcessSignal(CMessage *pM)
{
    std::string lPath=pM->GetPath();
    std::string lInterface=pM->GetInterface();
    
    bool lDispatched=false;
    
    auto lObjectMatch=mObjectMatches.find(lPath);
    if(lObjectMatch!=mObjectMatches.end())
    {
        tSignal *lSig=lObjectMatch->second;
        if(lSig->Count())
        {
            (*lSig)(pM);
            lDispatched=true;
        }
        else
        {
            delete lSig;
            std::string lRule=GetObjectMatchRule(lPath);
            dbus_bus_remove_match(mP->mConnection,lRule.c_str(),nullptr);
            mObjectMatches.erase(lObjectMatch);
        }
    }
    
    auto lInterfaceMatch=mInterfaceMatches.find(lPath+lInterface);
    if(lInterfaceMatch!=mInterfaceMatches.end())
    {
        tSignal *lSig=lInterfaceMatch->second;
        if(lSig->Count())
        {
            (*lSig)(pM);
            lDispatched=true;
        }
        else
        {
            delete lSig;
            std::string lRule=GetInterfaceMatchRule(lPath,lInterface);
            dbus_bus_remove_match(mP->mConnection,lRule.c_str(),nullptr);
            mObjectMatches.erase(lInterfaceMatch);
        }
    }

    return lDispatched;
}

CMessage *CBus::GetQueuedReply(int pSerial)
{
    auto i=mPendingReplies.find(pSerial);

    if(i!=mPendingReplies.end())
    {
        mPendingReplies.erase(i);
        return i->second;
    }
    return nullptr;
}

uint32_t CBus::SendMessage(CMessage *pM)
{
    dbus_uint32_t lSerial;
    dbus_connection_send(mP->mConnection,pM->mP->mMessage,&lSerial);
    return lSerial;
}

PMessage CBus::SendMessageEnqueue(CMessage *pM)
{
    dbus_uint32_t lSerial;
    dbus_connection_send(mP->mConnection,pM->mP->mMessage,&lSerial);

    PMessage lReply(new CMessage(nEmptyReply,this,lSerial));
    mPendingReplies[lSerial]=lReply.get();
    return lReply;
}

PMessage CBus::SendMessageWait(CMessage *pM)
{
    DBusError lError;
    dbus_error_init(&lError);
    DBusMessage *lReturn=dbus_connection_send_with_reply_and_block(mP->mConnection,pM->mP->mMessage,-1,&lError);
    if(!lReturn)
        throw XError(lError.name,lError.message);
    
    PMessage lReply(new CMessage(nReceived,this,lReturn));
    return lReply;
}

CBus *get_bus(EBusType pType)
{
    switch(pType)
    {
    case SESSION:
        return get_session_bus();
    case SYSTEM:
        return get_system_bus();
    default:
        throw XArgument("invalid DBus bus requested");
    }
}

CBus *get_session_bus()
{
    static CBus *lBus=new CBus(SESSION);

    return lBus;
}

CBus *get_system_bus()
{
    static CBus *lBus=new CBus(SYSTEM);

    return lBus;
}

/*namespace dbus*/ } /*namespace buola*/ }
